16. 缺失值处理:长三角上市公司验证(英国零售对照)

本章概要

  • 学习材料:2019—2025长三角公司财报快照、公司主表与收入/净利润字段。
  • 本章任务:运行 lst-ch16-local-financial-quality,按公司—报告期检查缺失并拓展应用到广东省。
  • 完成后你将得到:公司数、季度行数、两字段缺失率和广东拓展应用对照表。
  • 自我检查:手算“缺失数÷总行数”,并确认报告期、字段和公司代码均符合本章说明。
  • 拓展练习:把同一检查拓展应用到另一省市或另一财报字段。

本章学习目标

  • 理解缺失值的三种缺失机制(MCAR、MAR、MNAR)
  • 掌握缺失值的检测与统计方法
  • 学会使用删除法与填充法处理缺失值
  • 区分本地真实数据依据、受保护平台练习与确定性课堂示例

核心与扩展路线

单元 必须掌握(每单元≤3项) 中途检查
A|识别 MCAR/MAR/MNAR;数量/比例;披露状态 给三种缺失情景分类并说明依据
B|决策 删除、标识、原始/处理双口径 对一个字段写出“处理/停止”规则
C|实训 读取本地财报;筛选期间和地区;比较不同设置 独立计算两个字段的缺失率

多项式、滚动和分组插补属于课后扩展,不影响本章核心学习要求。

核心单元A|作答→反馈

先作答:把“未披露、系统漏采、业务不适用”分别归入可确认/待调查的缺失机制,并写出所需依据。
反馈:仅凭空值本身不能判定 MCAR/MAR/MNAR;合格答案会引用披露状态、采集日志或业务适用规则,并把无依据情形标为待调查。

核心单元B|作答→反馈

先作答:为营业收入和净利润各写一条“保留缺失/停止/允许处理”规则。
反馈:财报未披露不能直接填 0 或均值;应保留原始缺失标记并说明处理方法。若无法确认报告版本或计算分母,应先查清后再计算。

核心单元C|作答→反馈

先作答:预测 2019—2025 长三角财报两字段的缺失率表应有哪些行列与检查项目。
反馈:至少含字段名、样本量、报告期、原始缺失数/率、2%/5%/10%/20%/30%情景状态;不得写出三行模拟表替代真实HDF检查。

中国主案例:长三角上市公司财报完整性

要素 课堂口径
本地资产 /home/ubuntu/r2_data_mount/data/stock/financial_statement.h5(key: financial_data
样本 2019–2025 年沪苏浙皖上市公司季度财报
关键字段 ts_codeend_date、营业收入、净利润
决策主体 投研数据负责人:决定公司是否进入可比样本

关键字段缺失率超过 5% 先核对披露状态;超过 20% 不做机械插补,退回数据源核对。英国零售数据仅保留为平台方法对照。

本地财报分析:读取、连接与缺失率核对

Listing 1: 长三角财报缺失率最小完整过程
展开本地财报核验代码
from pathlib import Path  # 导入Path以兼容Linux与Windows数据根目录
import pandas as pd  # 导入Pandas用于读取和连接本地财报

linux_root = Path('/home/ubuntu/r2_data_mount/data')  # 指定Linux课程数据根目录
windows_root = Path(r'C:\qiufei\data')  # 指定Windows课程数据根目录
data_root = linux_root if linux_root.exists() else windows_root  # 选择当前平台可用根目录
financial_path = data_root / 'stock' / 'financial_statement.h5'  # 定位财务报表快照
basic_path = data_root / 'stock' / 'stock_basic_data.h5'  # 定位公司地区基础表

if financial_path.exists() and basic_path.exists():  # 仅在课程数据挂载存在时执行实证
    financials = pd.read_hdf(financial_path, key='financial_data', columns=['order_book_id','quarter','operating_revenue','net_profit']).rename(columns={'order_book_id':'ts_code','quarter':'end_date','operating_revenue':'total_revenue','net_profit':'n_income'})  # 只读取必需财报字段并标准化
    companies = pd.read_hdf(basic_path, key='stock_basic_info', columns=['order_book_id','province']).rename(columns={'order_book_id':'ts_code','province':'area'})  # 只读取地区筛选字段并标准化
    financials['end_date'] = pd.PeriodIndex(financials['end_date'].str.upper(), freq='Q').end_time.normalize()  # 将季度键转换为报告期末日期
    companies['area'] = companies['area'].astype('string').str.replace(r'[省市]$','',regex=True)  # 将上海市、江苏省等标签统一为省市简称
    region_column = next(column for column in ['province', 'area', '地区'] if column in companies.columns)  # 识别地区字段
    target_regions = ['上海', '江苏', '浙江', '安徽']  # 定义长三角四省市口径
    regional_codes = companies.loc[companies[region_column].isin(target_regions), 'ts_code']  # 筛选长三角公司代码
    regional_financials = financials.loc[financials['ts_code'].isin(regional_codes) & financials['end_date'].between('2019-01-01', '2025-12-31')]  # 同时落实地区与2019—2025期间口径
    if regional_codes.empty or regional_financials.empty: raise ValueError('输入文件、字段、样本量或数值不符合当前分析要求,请按本页说明检查')  # 空地区或空期间不得发布NaN依据
    quality_report = regional_financials[['total_revenue', 'n_income']].isna().mean().rename('missing_rate')  # 计算关键字段缺失率
    if quality_report.empty or not quality_report.notna().all(): raise ValueError('输入文件、字段、样本量或数值不符合当前分析要求,请按本页说明检查')  # 缺失率必须非空且有限
    print(f'样本量:{len(regional_financials)};报告期:{regional_financials["end_date"].min()}{regional_financials["end_date"].max()}')  # 输出样本边界供核对
    print(quality_report.to_frame())  # 输出便于核对的数据质量结果
else:  # 其他运行环境未提供课程数据时保持明确提示
    print(f'本地数据挂载不可用:{data_root}')  # 仅在路径确实不可用时记录原因
样本量:53445;报告期:2019-03-31 00:00:00 至 2025-12-31 00:00:00
               missing_rate
total_revenue      0.003162
n_income           0.003143

阈值来源:5% 与 20% 均为课堂情景假设,不是行业统一标准;作业需同时报告 2%/10% 与 10%/30% 两组敏感性结果。

本地分析结果与说明

  • 参考结果:样本量、最早/最晚报告期,以及 total_revenuen_income 两项缺失率。公开教材读者可从课程数据下载入口取得财务报表数据;若读取失败,请核对文件位置和字段后重新运行。
  • 结果表:每个字段列出缺失率、课堂判断标准、处理建议,以及需要再次检查的情况。

什么是缺失值?

  • 缺失值(Missing Values) 是数据质量问题中最常见的挑战
  • 在真实数据分析中,几乎所有数据集都会包含缺失值
  • 错误处理缺失值会导致分析结果失真、决策偏差

常见产生原因:

  • 数据录入遗漏或系统故障
  • API采集失败、存储损坏
  • 业务逻辑导致(如停牌、未营业)

三种缺失机制

缺失类型 英文缩写 含义
完全随机缺失 MCAR 缺失与任何变量无关
随机缺失 MAR 缺失仅依赖已观测数据
非随机缺失 MNAR 缺失与缺失值本身相关

理解缺失机制是选择处理策略的前提。

错误处理的后果

  • 有偏估计: 删除MNAR数据会系统性扭曲样本
  • 信息损失: 过度删除会减少样本量,降低统计功效
  • 虚假结论: 不当填充可能创造不存在的模式

运行前预测|平台任务解答代码

  • 输入预测:运行前先写出 pathdf 的业务含义、数据类型或取值范围,并判断哪一个输入最可能改变结果。
  • 结果预测:不展开答案,先预测将得到df 的结果;同时写出方向、数量级或表格/图形结构。
  • 完成要求:能独立说明本任务从输入到“平台任务解答代码”结果的关键步骤,原样录入平台代码并得到可核对的运行结果。

⭐ 平台任务解答代码

# ⚠️ 平台原始代码 - 请原样输入至教学平台(注释除外),平台才会判定答案正确
# 注:英国零售商data.csv数据文件本地没有,但平台已经内置
import pandas as pd  # 导入Pandas数据分析库
import numpy as np  # 导入NumPy数值计算库
path= '英国零售商data.csv'  # 设置path为"英国零售商data.csv"
df=pd.read_csv(path,dtype={'CustomerID':str,'InvoiceID':str})  # 从CSV文件读取数据存入df
# 剔除CustomerID的缺失数据
df.dropna(subset=['CustomerID'],inplace=True)
print(df.CustomerID.count())  # 输出非缺失值计数

任务复盘|平台任务解答代码

运行后核对:核对 pathdf 是否按预测参与运算,实际输出是否与预测一致;若不一致,先检查类型、单位、索引/字段和运算顺序。

拓展练习:把输入表替换为本地中国上市公司数据的同结构子集;指出必须保持的字段、数据类型和质量检查。

缺失值检测:创建示例数据

Listing 2: 创建包含缺失值的示例数据
展开示例数据代码
import pandas as pd
import numpy as np

# 6家零售门店(A-F)的日销售数据
data = {
    'Store': ['A', 'B', 'C', 'D', 'E', 'F'],
    # 门店B和E的销售额缺失(系统故障或未营业)
    'Sales': [100, np.nan, 150, 200, np.nan, 180],
    # 门店C和F的利润缺失(成本数据未录入)
    'Profit': [10, 15, np.nan, 20, 25, np.nan],
    # 门店D的客户数缺失(客流计数器故障)
    'Customers': [50, 75, 100, np.nan, 80, 95]
}
df = pd.DataFrame(data)
print('原始数据:')
print(df)
原始数据:
  Store  Sales  Profit  Customers
0     A  100.0    10.0       50.0
1     B    NaN    15.0       75.0
2     C  150.0     NaN      100.0
3     D  200.0    20.0        NaN
4     E    NaN    25.0       80.0
5     F  180.0     NaN       95.0

检测方法1:isnull() 识别缺失位置

Listing 3: isnull()检测缺失值位置
# df.isnull():返回布尔型数据框
# True表示该位置是缺失值,False表示不是
print('缺失值位置(True表示缺失):')
print(df.isnull())
缺失值位置(True表示缺失):
   Store  Sales  Profit  Customers
0  False  False   False      False
1  False   True   False      False
2  False  False    True      False
3  False  False   False       True
4  False   True   False      False
5  False  False    True      False
  • isnull()isna() 功能完全相同,可互换使用
  • 返回与原数据框同形状的布尔矩阵

检测方法2:统计每列缺失值数量

Listing 4: 统计每列缺失值数量
# isnull()返回布尔值(True=1, False=0),sum()将True值相加
print('各列缺失值数量:')
print(df.isnull().sum())
各列缺失值数量:
Store        0
Sales        2
Profit       2
Customers    1
dtype: int64
  • 这是评估数据质量的关键指标
  • 可快速识别哪些变量问题最严重

检测方法3:缺失值比例与完整行数

Listing 5: 缺失值比例与完整行统计
展开缺失率统计代码
# 计算每列缺失值占总行数的百分比
print('各列缺失值比例(%):')
print(df.isnull().mean() * 100)
print()

# 完整行数(无任何缺失的行)
print('完整行数(无缺失):', df.dropna().shape[0])
print('总行数:', df.shape[0])
各列缺失值比例(%):
Store         0.000000
Sales        33.333333
Profit       33.333333
Customers    16.666667
dtype: float64

完整行数(无缺失): 1
总行数: 6

决策阈值参考:

  • 缺失率 < 5%:可简单删除或填充
  • 缺失率 5%~20%:需谨慎选择策略
  • 缺失率 > 20%:考虑删除变量或高级插补

缺失模式可视化:创建模拟数据

Listing 6: 创建含不同缺失模式的数据
展开缺失模式构造代码
import pandas as pd
import numpy as np

np.random.seed(42)
n_rows = 100
data_vis = pd.DataFrame({
    'A': np.random.rand(n_rows),
    'B': np.random.rand(n_rows),
    'C': np.random.rand(n_rows),
    'D': np.random.rand(n_rows)
})

# 场景1:变量A的10%随机缺失(MCAR)
mask_a = np.random.rand(n_rows) < 0.1
data_vis.loc[mask_a, 'A'] = np.nan

# 场景2:变量B的条件缺失(MAR:当A<0.5时B缺失)
data_vis.loc[data_vis['A'] < 0.5, 'B'] = np.nan

# 场景3:变量C的15%随机缺失
mask_c = np.random.rand(n_rows) < 0.15
data_vis.loc[mask_c, 'C'] = np.nan

# 场景4:变量D无缺失(对照组)
missing_matrix = data_vis.isnull()
print('各变量缺失值数量:')
print(missing_matrix.sum())
各变量缺失值数量:
A    16
B    43
C    15
D     0
dtype: int64

缺失模式热力图与条形图

展开可视化代码
import matplotlib.pyplot as plt

plt.rcParams['font.sans-serif'] = ['Source Han Serif SC']
plt.rcParams['axes.unicode_minus'] = False

fig, axes = plt.subplots(1, 2, figsize=(11, 4))  # 使用紧凑横向画布以适配课堂投影

# 子图1:缺失模式热力图(红色=缺失)
axes[0].imshow(missing_matrix.T, aspect='auto', cmap='Reds', interpolation='none')
axes[0].set_yticks(range(len(data_vis.columns)))
axes[0].set_yticklabels(data_vis.columns)
axes[0].set_xlabel('观测序号')
axes[0].set_title('缺失模式热力图(红色=缺失)')
axes[0].grid(False)

# 子图2:缺失比例条形图
missing_ratios = missing_matrix.sum() / len(data_vis) * 100
axes[1].bar(range(len(data_vis.columns)), missing_ratios,
           color='steelblue', edgecolor='black')
axes[1].set_xticks(range(len(data_vis.columns)))
axes[1].set_xticklabels(data_vis.columns)
axes[1].set_ylabel('缺失比例(%)')
axes[1].set_title('各变量缺失比例')
axes[1].grid(axis='y', alpha=0.3)

for i, v in enumerate(missing_ratios):
    axes[1].text(i, v + 1, f'{v:.1f}%', ha='center', fontsize=16)

plt.tight_layout()
plt.show()
图中展示缺失模式热力图与各变量缺失比例;读者应依据坐标、图例与注释比较主要模式。
Figure 1: 缺失模式热力图与各变量缺失比例

删除法概述

删除法(Deletion) 是最简单的缺失值处理方式。

适用条件:

  • 数据缺失是完全随机的(MCAR)
  • 删除后样本量仍充足
  • 缺失比例较低(通常 < 5%)

风险提示: 在时间序列中直接删除可能导致时间不连续、幸存者偏差。

删除策略1:删除含任何缺失值的行

Listing 7: 删除含任何缺失值的行
展开删除策略代码
import pandas as pd
import numpy as np

data = {
    'Store': ['A', 'B', 'C', 'D', 'E'],
    'Sales': [100, np.nan, 150, 200, np.nan],
    'Profit': [10, 15, np.nan, 20, 25]
}
df = pd.DataFrame(data)
print('原始数据:')
print(df)
print()

# how='any':只要行中有任何一个缺失值,就删除该行
df_drop_any = df.dropna(how='any')
print('删除含任何缺失值的行(how="any"):')
print(df_drop_any)
原始数据:
  Store  Sales  Profit
0     A  100.0    10.0
1     B    NaN    15.0
2     C  150.0     NaN
3     D  200.0    20.0
4     E    NaN    25.0

删除含任何缺失值的行(how="any"):
  Store  Sales  Profit
0     A  100.0    10.0
3     D  200.0    20.0

删除策略2:只删除全部缺失的行

Listing 8: 只删除全部缺失的行
# how='all':只有当行中所有值都缺失时,才删除
df_drop_all = df.dropna(how='all')
print('只删除全部缺失的行(how="all"):')
print(df_drop_all)
只删除全部缺失的行(how="all"):
  Store  Sales  Profit
0     A  100.0    10.0
1     B    NaN    15.0
2     C  150.0     NaN
3     D  200.0    20.0
4     E    NaN    25.0
  • 这是一种宽松策略,保留部分数据
  • 只有整行全部缺失时才会被删除

删除策略3:按特定列删除

Listing 9: 删除特定列含缺失值的行
# subset=['Sales']:只检查Sales列是否缺失
df_drop_subset = df.dropna(subset=['Sales'])
print('只删除Sales列缺失的行:')
print(df_drop_subset)
只删除Sales列缺失的行:
  Store  Sales  Profit
0     A  100.0    10.0
2     C  150.0     NaN
3     D  200.0    20.0
  • 核心变量缺失时,整行数据可能无意义
  • 其他列缺失不影响删除决策

删除策略4:删除含缺失值的列

Listing 10: 删除含缺失值的列
# axis=1:按列操作(删除包含缺失值的列)
df_drop_cols = df.dropna(axis=1)
print('删除含缺失值的列:')
print(df_drop_cols)
删除含缺失值的列:
  Store
0     A
1     B
2     C
3     D
4     E
  • 当某列缺失率太高(如 >30%),可考虑删除该列
  • 注意:会丢失该变量的所有信息

填充法概述

填充法(Imputation) 用估计值替代缺失值,保留所有观测。

方法 适用场景 优缺点
零填充 计数类数据 简单但通常不推荐
均值填充 横截面数据 低估方差
中位数填充 含极端值数据 对异常值稳健
前向填充 时间序列 保持状态持续
后向填充 时间序列 可能引入前视偏差
线性插值 高频数据 假设平滑变化

创建金融时间序列示例

Listing 11: 创建含缺失值的金融时间序列
展开实现代码
import pandas as pd
import numpy as np

# 10天的价格序列,含3个缺失值
dates = pd.date_range('2024-01-01', periods=10)
prices = [100.5, np.nan, 101.2, 100.8, np.nan, np.nan, 102.5, 103.0, np.nan, 104.2]
df_prices = pd.DataFrame({'Date': dates, 'Price': prices})
print('原始价格数据:')
print(df_prices)
原始价格数据:
        Date  Price
0 2024-01-01  100.5
1 2024-01-02    NaN
2 2024-01-03  101.2
3 2024-01-04  100.8
4 2024-01-05    NaN
5 2024-01-06    NaN
6 2024-01-07  102.5
7 2024-01-08  103.0
8 2024-01-09    NaN
9 2024-01-10  104.2

填充策略1:零填充与均值填充

Listing 12: 零填充与均值填充
# 策略1:零填充(通常不推荐,价格不可能是0)
df_zero = df_prices.copy()
df_zero['Price'] = df_zero['Price'].fillna(0)
print('零填充(通常不推荐):')
print({'填充值': 0, '填充后缺失数': int(df_zero['Price'].isna().sum())})

# 策略2:均值填充
mean_price = df_prices['Price'].mean()
df_mean = df_prices.copy()
df_mean['Price'] = df_mean['Price'].fillna(mean_price)
print(f'均值填充(均值={mean_price:.2f}):')
print({'填充值': round(mean_price, 2), '填充后缺失数': int(df_mean['Price'].isna().sum())})
零填充(通常不推荐):
{'填充值': 0, '填充后缺失数': 0}
均值填充(均值=102.03):
{'填充值': 102.03, '填充后缺失数': 0}

填充策略2:中位数填充

Listing 13: 中位数填充
# 中位数比均值更稳健,不受极端值影响
median_price = df_prices['Price'].median()
df_median = df_prices.copy()
df_median['Price'] = df_median['Price'].fillna(median_price)
print(f'中位数填充(中位数={median_price:.2f}):')
print(df_median['Price'].values)
中位数填充(中位数=101.85):
[100.5  101.85 101.2  100.8  101.85 101.85 102.5  103.   101.85 104.2 ]
  • 中位数不受极端值(如涨跌停价格)影响
  • 适用于数据分布有偏的场景

填充策略3:前向填充(ffill)

Listing 14: 前向填充
展开前向填充代码
# method='ffill':用前一个有效值填充
df_ffill = df_prices.copy()
df_ffill['Price'] = df_ffill['Price'].fillna(method='ffill')
print('前向填充(用前一个值填充):')
print(df_ffill[['Date', 'Price']].head(4))
前向填充(用前一个值填充):
        Date  Price
0 2024-01-01  100.5
1 2024-01-02  100.5
2 2024-01-03  101.2
3 2024-01-04  100.8
  • 金融应用: 填充停牌期间的价格(假设价格保持不变)
  • 复牌时收益率包含停牌期间全部变化

填充策略4:后向填充(bfill)

Listing 15: 后向填充
展开后向填充代码
# method='bfill':用后一个有效值填充
df_bfill = df_prices.copy()
df_bfill['Price'] = df_bfill['Price'].fillna(method='bfill')
print('后向填充(用后一个值填充):')
print(df_bfill[['Date', 'Price']].head(4))
后向填充(用后一个值填充):
        Date  Price
0 2024-01-01  100.5
1 2024-01-02  101.2
2 2024-01-03  101.2
3 2024-01-04  100.8
  • 使用了「未来」的信息,可能导致 前视偏差
  • 在金融数据中较少使用

填充策略5:线性插值

Listing 16: 线性插值
展开线性插值代码
# interpolate(method='linear'):在两个已知点之间线性插值
df_linear = df_prices.copy()
df_linear['Price'] = df_linear['Price'].interpolate(method='linear')
print('线性插值:')
print(df_linear[['Date', 'Price']].head(4))
线性插值:
        Date   Price
0 2024-01-01  100.50
1 2024-01-02  100.85
2 2024-01-03  101.20
3 2024-01-04  100.80
  • 假设两个已知点之间变化是线性
  • 适用于高频数据的小缺口填充

线性插值的数学公式

对于时间点 \(t_1\)\(t_3\) 之间的缺失值 \(t_2\)

\[ \large y_{t_2} = y_{t_1} + \frac{t_2 - t_1}{t_3 - t_1} (y_{t_3} - y_{t_1}) \]

  • 等价于在两个已知点之间画一条直线
  • 假设价格在缺失期间平滑变化

高级填充:时间插值与多项式插值

Listing 17: 时间插值与多项式插值
展开高级插值代码
import pandas as pd
import numpy as np

np.random.seed(42)
dates = pd.date_range('2024-01-01', periods=50)
prices = 100 + np.random.randn(50).cumsum()
prices[10:15] = np.nan  # 模拟连续5天停牌
prices[30:32] = np.nan  # 模拟连续2天缺失
prices[45] = np.nan      # 模拟单日缺失

df = pd.DataFrame({'Date': dates, 'Price': prices})
df.set_index('Date', inplace=True)

# 策略1:时间插值(考虑日期间隔)
df_time = df.interpolate(method='time')

# 策略2:多项式插值(二阶)
df_poly = df.interpolate(method='polynomial', order=2)

interpolation_check = pd.concat([df_time.rename(columns={'Price': '时间插值'}), df_poly.rename(columns={'Price': '多项式插值'})], axis=1).iloc[[9, 10, 14, 15]]  # 选取缺口边界行形成便于核对摘要
print('连续缺口边界的两种插值结果:')  # 标明摘要对应的业务检查点
print(interpolation_check)  # 输出缺口前后与缺口内部的代表性结果
连续缺口边界的两种插值结果:
                  时间插值       多项式插值
Date                              
2024-01-10  104.480611  104.480611
2024-01-11  103.665999  104.716334
2024-01-15  100.407552  100.861727
2024-01-16   99.592940   99.592940

高级填充:滚动均值填充

Listing 18: 滚动均值填充
展开滚动均值代码
# 用前5天的平均价格填充缺失值
df_rolling = df.fillna(df.rolling(window=5, min_periods=1).mean())
print('滚动均值填充(窗口=5):')
print(df_rolling.iloc[9:16])
滚动均值填充(窗口=5):
                 Price
Date                  
2024-01-10  104.480611
2024-01-11  104.116570
2024-01-12  104.275396
2024-01-13  104.209331
2024-01-14  104.480611
2024-01-15         NaN
2024-01-16   99.592940
  • 考虑局部趋势,比全局均值更合理
  • window=5:使用前5个数据点计算均值
  • min_periods=1:最少需要1个非缺失值

高级填充:分组填充

Listing 19: 分组前向填充
展开分组填充代码
import pandas as pd
import numpy as np

stocks = pd.DataFrame({
    'Date': list(pd.date_range('2024-01-01', periods=5)) * 2,
    'Stock': ['A'] * 5 + ['B'] * 5,
    'Price': [100, 101, np.nan, 103, 104,
              50, np.nan, 52, 53, np.nan]
})
missing_before = stocks.groupby('Stock')['Price'].apply(lambda series: series.isna().sum())  # 记录各股票填充前缺失数

# 按股票代码分组,分别前向填充
stocks['Price'] = stocks.groupby('Stock')['Price'].fillna(method='ffill')
missing_after = stocks.groupby('Stock')['Price'].apply(lambda series: series.isna().sum())  # 计算各股票填充后缺失数
group_fill_check = pd.DataFrame({'填充前缺失数': missing_before, '填充后缺失数': missing_after})  # 汇总分组填充的便于核对变化
print('分组填充质量摘要:')  # 标明输出用于检查组间隔离与缺失变化
print(group_fill_check)  # 输出每只股票填充前后的缺失数
分组填充质量摘要:
       填充前缺失数  填充后缺失数
Stock                
A           1       0
B           2       0
  • 关键: 不同股票的数据不能混用
  • 股票A的缺失值只能用股票A的数据填充

课堂示例:零售 字段 与缺失标识

Listing 20: 确定性课堂示例(非业务观测)
展开确定性示例数据与诊断代码
import pandas as pd
import numpy as np

# 确定性课堂示例:只用于演示字段结构与缺失处理,不代表任何企业或市场
df_retail = pd.DataFrame({
    'InvoiceID': ['FIX-01','FIX-02','FIX-03','FIX-04','FIX-05','FIX-06'],
    'CustomerID': ['C01','C02',np.nan,'C02','C03','C03'],
    'InvoiceDate': pd.date_range('2023-01-01', periods=6),
    'Quantity': [2,4,3,np.nan,5,1],
    'UnitPrice': [10.0,12.0,8.0,12.0,np.nan,9.0],
    'is_observed': [True] * 6
})
retail_quality = pd.DataFrame({'缺失数': df_retail.isnull().sum(), '缺失率(%)': df_retail.isnull().mean() * 100})  # 汇总字段级缺失规模与比例
print('课堂示例(非业务观测)形状:', df_retail.shape)  # 输出教学输入边界
print('\n存在缺失的字段:')  # 标明仅展示需要处置的字段
print(retail_quality.loc[retail_quality['缺失数'] > 0])  # 输出任务相关的精简质量诊断
课堂示例(非业务观测)形状: (6, 6)

存在缺失的字段:
            缺失数     缺失率(%)
CustomerID    1  16.666667
Quantity      1  16.666667
UnitPrice     1  16.666667

课堂示例:处理 CustomerID 缺失

Listing 21: 删除CustomerID缺失的行
# 复制数据,保留原始数据
df_clean = df_retail.copy()

# 决策:删除CustomerID缺失的行
# 理由:客户ID是关联交易的关键字段,无法合理推测
before_customer = len(df_clean)
df_clean.dropna(subset=['CustomerID'], inplace=True)
after_customer = len(df_clean)

print(f'CustomerID处理:')
print(f'  删除前: {before_customer} 行')
print(f'  删除后: {after_customer} 行')
print(f'  删除比例: {(before_customer - after_customer) / before_customer:.2%}')
CustomerID处理:
  删除前: 6 行
  删除后: 5 行
  删除比例: 16.67%

课堂示例:处理 Quantity 缺失

Listing 22: 用中位数填充Quantity
# 决策:用中位数填充(稳健,不受异常值影响)
median_quantity = df_clean['Quantity'].median()
df_clean['Quantity'] = df_clean['Quantity'].fillna(median_quantity)

print(f'Quantity处理:')
print(f'  填充值(中位数): {median_quantity}')
print(f'  剩余缺失: {df_clean["Quantity"].isnull().sum()}')
Quantity处理:
  填充值(中位数): 3.0
  剩余缺失: 0
  • 数量可能有极端值(大额批发),中位数更稳健
  • 用「典型订单量」替代缺失值

课堂示例:标记 UnitPrice 缺失

Listing 23: 按客户分组均值填充UnitPrice
# 无SKU、退货、币种与税口径,不能据客户均价推断真实单价
df_clean['unit_price_is_imputed'] = df_clean['UnitPrice'].isna()
df_clean['UnitPrice_imputed'] = df_clean['UnitPrice'].fillna(df_clean['UnitPrice'].median())

print(f'UnitPrice处理:')
print('  原值保留;另建课堂插补列与标识')
print(f'  原始缺失: {df_clean["UnitPrice"].isnull().sum()}')
UnitPrice处理:
  原值保留;另建课堂插补列与标识
  原始缺失: 1

课堂示例:最终数据质量验证

Listing 24: 处理后数据质量验证
print('=== 处理后数据质量 ===')
print(f'总行数: {len(df_clean)}')
print(f'原始字段完整行数: {df_clean[["CustomerID","Quantity","UnitPrice"]].dropna().shape[0]}')
print(f'\n最终缺失值统计:')
print(df_clean.isnull().sum())
=== 处理后数据质量 ===
总行数: 5
原始字段完整行数: 4

最终缺失值统计:
InvoiceID                0
CustomerID               0
InvoiceDate              0
Quantity                 0
UnitPrice                1
is_observed              0
unit_price_is_imputed    0
UnitPrice_imputed        0
dtype: int64

数据质量报告

Listing 25: 生成数据质量报告
展开质量报告代码
# 原始/课堂插补双口径;不得据示例数据发布金额或客户业务结论
df_clean['amount_observed'] = df_clean['Quantity'] * df_clean['UnitPrice']
df_clean['amount_fixture_imputed'] = df_clean['Quantity'] * df_clean['UnitPrice_imputed']
quality_summary = pd.Series({'示例数据记录数': len(df_clean), '原始金额可计算行': df_clean['amount_observed'].notna().sum(), '课堂插补行': df_clean['unit_price_is_imputed'].sum(), '依据等级': '确定性课堂示例,非业务观测'})
print('=== 课堂数据质量摘要 ===')
print(quality_summary.to_string())  # 逐项输出样本边界与核心交易指标
=== 课堂数据质量摘要 ===
示例数据记录数                 5
原始金额可计算行                4
课堂插补行                   1
依据等级        确定性课堂示例,非业务观测

禁止外推:示例数据不能生成客户排名

Listing 26: Top 5客户统计
required_business_fields = {'SKU','Currency','TaxRule','ReturnFlag'}
missing_contract = required_business_fields - set(df_clean.columns)
print('输入文件、字段、样本量或数值不符合当前分析要求,请按本页说明检查', sorted(missing_contract))
输入文件、字段、样本量或数值不符合当前分析要求,请按本页说明检查 ['Currency', 'ReturnFlag', 'SKU', 'TaxRule']

课堂示例:停牌处理的反事实演示

Listing 27: 确定性价格路径(非市场观测)
展开停牌数据构造代码
import pandas as pd
import matplotlib.pyplot as plt

plt.rcParams['font.sans-serif'] = ['Source Han Serif SC']
plt.rcParams['axes.unicode_minus'] = False

dates = pd.date_range('2024-01-01', periods=30)
prices_normal = np.array([100,101,100.5,101.2,101.0,102,101.7,102.4,102.2,103,103.1,102.9,103.2,103.4,103.6,104,103.8,104.2,104.5,104.1,104.6,104.8,105,105.2,104.9,105.3,105.5,105.4,105.8,106.0])  # 确定性课堂路径

# 创建停牌缺失
prices_suspended = prices_normal.copy()
prices_suspended[10:15] = np.nan  # 第一次停牌5天
prices_suspended[22:24] = np.nan  # 第二次停牌2天

df_stock = pd.DataFrame({
    'Date': dates,
    'Price': prices_suspended,
    'Price_Normal': prices_normal
})

print(f'缺失天数: {df_stock["Price"].isnull().sum()}')
print(f'缺失比例: {df_stock["Price"].isnull().mean():.2%}')
缺失天数: 7
缺失比例: 23.33%

停牌处理:不同策略对比

Listing 28: 停牌处理示例数据的填充与收益率字段
展开完整参考实现
# 不同填充策略
df_stock['ForwardFill'] = df_stock['Price'].ffill()  # 用历史可得价格执行前向填充
df_stock['LinearInterp'] = df_stock['Price'].interpolate(method='linear')  # 用线性插值生成仅供比较的替代路径
df_stock['Return_Normal'] = df_stock['Price_Normal'].pct_change()  # 计算示例数据完整路径的日收益率
df_stock['Return_FF'] = df_stock['ForwardFill'].pct_change()  # 计算前向填充路径的日收益率
df_stock['Return_Lin'] = df_stock['LinearInterp'].pct_change()  # 计算线性插值路径的日收益率
missing_rows = df_stock['Price'].isnull()  # 标记停牌示例数据中的缺失日期
展开完整参考实现
from matplotlib import dates as mdates  # 导入日期刻度工具以减少横轴拥挤
fig, axes = plt.subplots(2, 1, figsize=(12, 4.5), sharex=True)  # 在实际发图单元创建共享日期轴双面板画布
axes[0].plot(df_stock['Date'], df_stock['Price_Normal'], 'g-', label='示例数据完整路径(仅供误差对照)', linewidth=2)  # 绘制完整价格示例数据
axes[0].plot(df_stock['Date'], df_stock['ForwardFill'], 'b--', label='前向填充', linewidth=1.5)  # 绘制前向填充价格
axes[0].plot(df_stock['Date'], df_stock['LinearInterp'], 'r--', label='线性插值', linewidth=1.5, alpha=0.7)  # 绘制线性插值价格
axes[0].scatter(df_stock.loc[missing_rows, 'Date'], df_stock.loc[missing_rows, 'Price_Normal'], color='red', s=50, zorder=5, label='示例数据隐藏值(现实中不可观测)')  # 标示仅用于误差核对的隐藏值
axes[0].set_ylabel('价格', fontsize=16)  # 标注价格轴
axes[0].set_title('股票停牌处理示例数据:价格对比', fontsize=16)  # 标明依据为课堂示例
axes[0].legend(fontsize=16,loc='center left',bbox_to_anchor=(1.01,.5),title='处理路径',title_fontsize=16)  # 将上面板图例移出绘图区避免遮挡曲线、红点与日期
axes[0].grid(True, alpha=0.3)  # 添加辅助网格
axes[1].plot(df_stock['Date'], df_stock['Return_Normal'], 'g-',
            label='示例数据完整路径收益率', linewidth=2)  # 绘制完整路径收益率
axes[1].plot(df_stock['Date'], df_stock['Return_FF'], 'b--',
            label='前向填充收益率', linewidth=1.5)  # 绘制前向填充收益率
axes[1].plot(df_stock['Date'], df_stock['Return_Lin'], 'r--',
            label='线性插值收益率', linewidth=1.5, alpha=0.7)  # 绘制线性插值收益率
axes[1].axhline(y=0, color='k', linestyle='-', linewidth=0.5)  # 标出零收益基线
axes[1].set_xlabel('日期', fontsize=16)  # 标注日期轴
axes[1].set_ylabel('日收益率', fontsize=16)  # 标注收益率轴
axes[1].set_title('股票停牌处理示例数据:收益率对比', fontsize=16)  # 标明依据为课堂示例
axes[1].legend(fontsize=16,loc='center left',bbox_to_anchor=(1.01,.5),title='收益率路径',title_fontsize=16)  # 将下方面板图例移出绘图区避免遮挡收益线
axes[1].grid(True, alpha=0.3)  # 添加辅助网格
axes[1].xaxis.set_major_locator(mdates.YearLocator(5))  # 每五年显示一个主刻度以减少日期拥挤
axes[1].xaxis.set_major_formatter(mdates.DateFormatter('%Y'))  # 用四位年份格式化共享日期轴
axes[0].tick_params(axis='both',labelsize=16)  # 保证上面板刻度可读
axes[1].tick_params(axis='both',labelsize=16)  # 保证下面板刻度可读
plt.tight_layout(rect=(0,0,.70,1))  # 为右侧两面板图例预留独立空间
plt.show()  # 在当前单元发出完整图形
上下两幅图比较停牌示例数据的完整路径、前向填充与线性插值所得到的价格和日收益率。
Figure 2: 股票停牌数据处理:价格与收益率对比

停牌处理:误差评估

Listing 29: 填充策略误差评估
# 计算均方误差(MSE)
mse_ff = ((df_stock['ForwardFill'] - df_stock['Price_Normal']) ** 2).mean()
mse_lin = ((df_stock['LinearInterp'] - df_stock['Price_Normal']) ** 2).mean()

print('填充误差评估:')
print(f'前向填充 MSE: {mse_ff:.4f}')
print(f'线性插值 MSE: {mse_lin:.4f}')
print(f'\nMSE越小,填充效果越好。')
填充误差评估:
前向填充 MSE: 0.0260
线性插值 MSE: 0.0182

MSE越小,填充效果越好。

停牌处理最佳实践

数据类型 推荐方法 理由
价格序列 前向填充 假设停牌期间价格不变
收益率序列 设为 0 或 NaN 停牌期间无收益
成交量序列 设为 0 停牌无交易

关键: 保留「停牌」标识,在计算统计量时排除停牌数据。

本章小结

  • 检测方法: isnull()isnull().sum()isnull().mean()
  • 删除法: dropna(how=, subset=, axis=)
  • 填充法: fillna()interpolate()
  • 高级方法: 滚动均值填充、分组填充
  • 核心原则: 根据缺失机制和业务逻辑选择策略

随堂练习

  • 问题 1|怎样完成本章分析?:独立运行 lst-ch16-local-financial-quality,写出 2019—2025 长三角公司数、季度行数、营业收入/净利润缺失数与缺失率,并抽一字段用“缺失数÷总行数”手算核对。
  • 问题 2|结果说明什么?:画出长三角上市公司财报从读取、整理到计算缺失率的步骤;选一个实际字段说明它的缺失率,并解释哪些情况可能造成误判。
  • 问题 3|换一个情境,怎样继续应用?:将同一检查步骤用于另一省市或另一财报字段,保持版本、期间与原始缺失标识不变;比较样本量和缺失率,并检查筛选后报告期仍在 2019—2025。
  • 作答提示:请完成三道题,并在回答中引用实际运行结果;拓展练习中不要把课堂示例写成普遍规律或因果结论。

教师参考解答

教师参考解答|答案与说明 1

  • 解答示例|真实 HDF 财报检查步骤:先运行本章 lst-ch16-local-financial-quality 得到 regional_financialsquality_report,再执行判断条件敏感性:

教师参考解答|代码 1

展开代码(代码区可独立滚动)
from pathlib import Path  # 导入路径工具以定位规定财报快照
import pandas as pd  # 导入表格工具以读取和连接HDF
data_root=Path('/home/ubuntu/r2_data_mount/data')  # 绑定受权课堂数据根目录
financial_path=data_root/'stock'/'financial_statement.h5'  # 定位财务报表快照
basic_path=data_root/'stock'/'stock_basic_data.h5'  # 定位公司地区基础表
if not financial_path.exists() or not basic_path.exists(): raise FileNotFoundError('未找到课程数据文件,请从课程数据下载入口获取并核对文件位置')  # 规定资产缺失时终止检查
financials=pd.read_hdf(financial_path,key='financial_data',columns=['order_book_id','quarter','operating_revenue','net_profit']).rename(columns={'order_book_id':'ts_code','quarter':'end_date','operating_revenue':'total_revenue','net_profit':'n_income'})  # 读取并标准化必需财报字段
companies=pd.read_hdf(basic_path,key='stock_basic_info',columns=['order_book_id','province']).rename(columns={'order_book_id':'ts_code','province':'area'})  # 读取并标准化公司地区字段
financials['end_date']=pd.PeriodIndex(financials['end_date'].str.upper(),freq='Q').end_time.normalize()  # 将季度键转为统一报告期末日
companies['area']=companies['area'].astype('string').str.replace(r'[省市]$','',regex=True)  # 将省市名统一为简称
region_column='area'  # 固定标准化后的地区字段
target_regions=['上海','江苏','浙江','安徽']  # 定义长三角四省市口径
regional_codes=companies.loc[companies[region_column].isin(target_regions),'ts_code']  # 提取长三角公司代码
regional_financials=financials.loc[financials['ts_code'].isin(regional_codes)&financials['end_date'].between('2019-01-01','2025-12-31')].copy()  # 按地区与期间形成检查样本
if regional_financials.empty: raise ValueError('输入文件、字段、样本量或数值不符合当前分析要求,请按本页说明检查')  # 样本为空时终止缺失率发布
quality_report=regional_financials[['total_revenue','n_income']].isna().mean().rename('missing_rate')  # 计算两个字段的原始缺失率

教师参考解答|代码 2

展开代码(代码区可独立滚动)
missing_counts=regional_financials[['total_revenue','n_income']].isna().sum()  # 统计两项真实字段缺失数
row_count=len(regional_financials)  # 记录真实HDF检查分母
manual_rates=missing_counts.div(row_count)  # 以缺失数除以总行数独立复算缺失率
assert manual_rates.equals(quality_report)  # 检查程序结果与手算结果完全一致
threshold_table=pd.DataFrame({'threshold':[.02,.05,.10,.20,.30]})  # 定义课堂敏感性阈值
for field_name in ['total_revenue','n_income']:  # 逐字段生成敏感性状态
    threshold_table[field_name]=threshold_table['threshold'].lt(quality_report[field_name]).map({True:'REVIEW',False:'PASS'})  # 比较缺失率与阈值
period_ok=regional_financials['end_date'].between('2019-01-01','2025-12-31').all()  # 核验固定报告期
print({'rows':row_count,'companies':regional_financials['ts_code'].nunique(),'period_ok':period_ok},missing_counts,quality_report,threshold_table)  # 输出真实检查依据

教师参考解答|答案与说明 2

  • 解释答案:长三角财报缺失可能代表未披露,不能自动填0或均值;须按公司×报告期×版本报告完整性。
  • 拓展应用答案|另一省份同口径核对:保留同一 HDF、字段与2019—2025期间,只把地区改为广东,并比较样本量与两字段缺失率:

教师参考解答|代码 3

展开代码(代码区可独立滚动)
alternative_region='广东'  # 仅替换地区对象以保持版本、期间与字段口径不变
alternative_codes=companies.loc[companies[region_column].eq(alternative_region),'ts_code']  # 提取新案例省份公司代码
alternative_financials=financials.loc[financials['ts_code'].isin(alternative_codes)&financials['end_date'].between('2019-01-01','2025-12-31')].copy()  # 按同一期间构造新案例样本
if alternative_financials.empty: raise ValueError('输入文件、字段、样本量或数值不符合当前分析要求,请按本页说明检查')  # 新案例对象为空时停止比较
alternative_quality=alternative_financials[['total_revenue','n_income']].isna().mean().rename('alternative_missing_rate')  # 按相同分母复算新案例缺失率
alternative_comparison=pd.concat([quality_report.rename('yangtze_delta_missing_rate'),alternative_quality],axis=1)  # 并列展示原对象与新案例对象缺失率
assert alternative_financials['end_date'].between('2019-01-01','2025-12-31').all()  # 检查新案例后仍严格命中固定期间
print({'source_rows':row_count,'alternative_rows':len(alternative_financials),'alternative_region':alternative_region},alternative_comparison)  # 输出新案例前后样本量与缺失率差异

教师参考解答|答案与说明 3

  • 所用数据与字段financial_statement.h5:financial_datastock_basic_data.h5:stock_basic_info;公司、地区、季度、营业收入、净利润及 2019—2025 期间。
  • 参考结果:真实行数、公司数、期间状态、两字段缺失数与缺失率、五档阈值状态,以及长三角—广东拓展应用前后的样本量与缺失率。所需文件由课程数据下载入口提供;若字段与章节说明不一致,请重新下载并核对文件版本。
  • 边界 / 局限:缺失率不识别缺失机制;未经披露依据不得插补。
  • 常见错误:用三行示例数据替代HDF;先插补再算缺失率;地区或期间筛选后不报分母。